#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
-------------------------------------------------------------------------------------------------
Copyright:	    This script or any part of this script is property of Etiger Capital. All rights reserved.
Author:		    Zhang Yong
CreateDate:	    2021-11-26
LastUpdate:	    2023-03-27
Version:	    3.0.0
Description:    Here is a collection of the generic functions/methods to manipulate Etiger Data.
                - the functions/methods offered in this package can be destructive;
                - the package or source code should NOT be redistributed to out of Etiger Capital;
Revision History:
    20211201, To handle the issue that factorCode not consistent with that in table names in Taos;
    20220616, Remove dummy code lines;
              Integrate the manipulation, storage of the factor values and meta data of equties and other asset types,
              ie. future, etc;
    20220627, Take into account module loading in Windows platform;
    20220630, HF factor values saving/update/delete;
    20220706, Make it optional if the metadata of a factor to be removed from DB when delete all data of a factor;
    20221215, Add timestamp for data update;
    20230216, API enhancement for factors assessment;
    20320221, use read_feather to replace read_dataframe;
    20230301, completely replace read_dataframe/write_dataframe with read_feather/write_feather;
    20230323, re-structure the code and minor change;
    20230327, Enhancement - authorization and authentication;
    20230506, Optimization for multi-thread coding;
-------------------------------------------------------------------------------------------------
"""
import os
from typing import List, Literal, Optional, Union
import pyarrow.feather as feather
import pandas as pd
from ETSDK import dataAccessLib as da
import platform
import datetime


class ETSDK:
    version = '2.4.2'
    
    def __init__(self, feather_root_path: str = None):
        self.token = ''
        self.user = 'Unknown'
        self.is_authenticated = False

        if feather_root_path:
            self.set_feather_root_path(feather_root_path)
        else:
            if platform.system().lower() == 'linux':
                self.FEATHER_ROOT_PATH = r"/mnt/nas/Etiger_Alpha/"
                # raise Exception("WARNING - FEATHER_ROOT_PATH NOT set correctly！")
            elif platform.system().lower() == 'windows':
                self.FEATHER_ROOT_PATH = r"//nas/public/Etiger_Alpha/"
                # self.FEATHER_ROOT_PATH = r"U:\Testing\ETSDK"
            else:
                raise Exception("WARNING - 因子数据操作模块当前仅被设计用于Linux和Windows环境！")

    def set_feather_root_path(self, path: str):
        if path:
            self.FEATHER_ROOT_PATH = path

    def auth(self, token: str):
        self.token = token
        ret_auth = da.executeSqlQueryInMYDB("call etiger_auth.pAuthenticateUser('{0}')".format(token))
        self.user = ret_auth['UserName'][0]
        self.is_authenticated = ret_auth['IsValid'][0]
        
        if not ret_auth['IsValid'][0]:
            msg = "Your token is outdated or not authenticated to access this API. Pls check with the administrator."
            raise Exception(msg)
        
    def auth_check(self):
        if not self.is_authenticated:
            msg = "ONLY the authenticated user can access this API. Make sure you've got a token and called the auth() method!"
            raise Exception(msg)
    
    # ========= 01 基础方法 =======================================================================
    # 创建因子分类的大类
    def create_factor_parent_category(self, asset_type: str, parent_category: str, parent_category_en: str):
        """创建股票因子分类的大类
    
        Parameters
        ----------
            asset_type : str
                资产类型，比如：Equity、Future、FixedIncome等
            parent_category : str
                大类名称
            parent_category_en : str
                大类英文名称
        """
        self.auth_check()
        
        sql_query = (
            "call etiger_core.pSDKCreateETFactorParentCategory('{0}', '{1}', '{2}');".format(
                asset_type, parent_category, parent_category_en
            )
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
        
    # 创建因子分类
    def create_factor_category(self, asset_type: str, parent_category: str, category: str, category_en: str):
        """创建股票因子分类
    
        Parameters
        ----------
            asset_type : str
                资产类型，比如：Equity、Future、FixedIncome等
            parent_category : str
                大类名称
            category : str
                分类名称
            category_en : str
                分类英文名称
        """
        self.auth_check()
        
        sql_query = (
            "call etiger_core.pSDKCreateETFactorCategory('{0}', '{1}', '{2}', '{3}');".format(
                asset_type, parent_category, category, category_en
            )
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 创建因子
    def create_et_factor(
        self,
        asset_type: str,
        parent_category: str,
        category: str,
        factor_code: str,
        description: str,
        level: str,
        suggested_update_time: str,
        remark: str = "",
        factor_type: str = "",
    ):
        """在弈泰数据平台上创建股票因子
    
        Parameters
        ----------
            asset_type: str, 资产类型，比如：Equity、Future、FixedIncome等
            parent_category : str, 因子大类中文名称
            category : str,因子类中文名称
            factor_code : str, 因子代码
            description : str, 有关该因子的描述
            level : str, 因子与其它因子的关系，衍生因子的层次
            suggested_update_time : str, 建议更新因子的时间
            remark : str, 备注或说明
            factor_type : str, 取值包括directional和relative_value，用于说明因子的取值属性，是定性的还是定量的
    
        Examples
        --------
            >>> ETSDK.create_et_factor(
            ...     asset_type="Equity",
            ...     parent_category="动量",
            ...     category="价格动量",
            ...     factor_code="DASREV",
            ...     description="分析师盈收预测...",
            ...     level="2",
            ...     suggested_update_time="T0 22:00",
            ...     remark="测试",
            ...     factor_type="relative_value",
            ... )
        """
        self.auth_check()
        
        sql_query = "call etiger_core.pSDKCreateETFactor('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}', " \
                    "'{9}');".format(
                                    asset_type,
                                    parent_category,
                                    category,
                                    factor_code,
                                    description,
                                    self.user,
                                    level,
                                    suggested_update_time,
                                    remark,
                                    factor_type)
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 删除因子
    def remove_et_factor(
        self,
        asset_type: str,
        factor_code: str
    ):
        """在弈泰数据平台上删除因子
    
        Parameters
        ----------
            asset_type: str, 资产类型，比如：Stock、Index、Equity、Future、FixedIncome等
            factor_code : str, 因子代码
    
        Examples
        --------
            >>> ETSDK.remove_et_factor(
            ...     asset_type="Future",
            ...     factor_code="test_sdk_05"
            ... )
        """
        self.auth_check()
    
        if asset_type.lower() in ["stock", "index"]:
            asset_type = 'Equity'
            
        sql_query = "call etiger_core.pSDKRemoveETFactor('{0}', '{1}', '{2}');".format(
            asset_type,
            factor_code,
            self.user
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 获取全部因子信息
    def get_et_factor(self) -> pd.DataFrame:
        """获取全部因子分类信息
        获取弈泰数据平台上所有来源的因子及其分类数据"""
        self.auth_check()
        
        conn = da.getFIDMySQLConnection()
        sql_query = "call etiger_core.pSDKGetETFactor('');"
    
        df = pd.read_sql(sql=sql_query, con=conn)
        conn.close()
        return df
    
    # 获取因子Id或FactorId
    def get_factor_id(self, asset_type: str, factor_code: str) -> Optional[int]:
        """获取因子Id或FactorId
    
        Parameters
        ----------
            asset_type: str, 资产类型，比如：Equity、Future、FixedIncome等
            factor_code : str, 因子代码
        """
        self.auth_check()
        
        if asset_type.lower() in ["stock", "index"]:
            asset_type = 'Equity'
            
        sql_query = "call etiger_core.pSDKGetETFactorId('{0}', '{1}');".format(asset_type, factor_code)
        df = da.executeSqlQueryInMYDB(sql_query)
    
        if not df.empty:
            return df.iloc[0, 0]
    
    # 更新因子
    def update_et_factor(
        self,
        factor_id: Union[str, int],
        category_id: Union[str, int] = None,
        factor_code: str = None,
        description: str = None,
        level: str = None,
        suggested_update_time: str = None,
        remark: str = None,
        factor_type: str = None,
    ):
        """更新因子，factor_id是主键，其它均可选
    
        Parameters
        ----------
            factor_id : str, 因子Id
            category_id : str, 分类Id
            factor_code : str, 因子代码
            description : str, 有关该因子的描述
            owner : str, 该因子的权利人
            level : str, 因子与其它因子的关系，衍生因子的层次，比如：'2'
            suggested_update_time : str, 建议更新因子的时间，比如：'T0 22:00'
            remark : str, 备注或说明
            factor_type : str, 取值包括directional和relative_value，用于说明因子的取值属性，是定性的还是定量的
            
        Examples
        --------
            >>> ETSDK.update_et_factor(factor_id=1169, factor_code='test_sdk_03', suggested_update_time='T0 20:00')
            >>> ETSDK.update_et_factor(factor_id=1169, level=2, owner='test')
            >>> ETSDK.update_et_factor(factor_id=1169, category_id=73)
        """
        self.auth_check()
        
        df = self.get_et_factor()
        df = df[df['FactorId'] == factor_id]
        df.reset_index(inplace=True)
        
        category_id_db = df.loc[0, 'CategoryId']
        factor_code_db = df.loc[0, 'FactorCode']
        description_db = df.loc[0, 'Description']
        level_db = df.loc[0, 'Level']
        suggested_update_time_db = df.loc[0, 'SuggestedUpdateTime']
        remark_db = df.loc[0, 'Remark']
        factor_type_db = df.loc[0, 'FactorType']
        
        if not category_id:
            category_id = category_id_db
        if not factor_code:
            factor_code = factor_code_db
        if not description:
            description = description_db
        if not level:
            level = level_db
        if not suggested_update_time:
            suggested_update_time = suggested_update_time_db
        if not remark:
            remark = remark_db
        if not factor_type:
            factor_type = factor_type_db
            
        sql_query = "call etiger_core.pSDKUpdateETFactor({0}, {1}, '{2}', '{3}', '{4}', '{5}', '{6}', '{7}');".format(
            str(factor_id),
            str(category_id),
            factor_code,
            description,
            level,
            suggested_update_time,
            remark,
            factor_type
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 停用因子
    def disable_et_factor(self, asset_type: str, factor_code: str):
        """停用股票因子
    
        Parameters
        ----------
            asset_type: str, 资产类型，比如：Equity、Future、FixedIncome等
            factor_code : str, 因子代码
        """
        self.auth_check()
        
        sql_query = "call etiger_core.pSDKDisableETFactor('{0}', '{1}', '{2}');".format(
            asset_type, factor_code, self.user
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    def __get_factor_years(self, path: str):
        self.auth_check()
        
        try:
            if not os.path.exists(path):
                # raise Exception("%s 路径不存在，请检查因子是否初始化" % path)
                print("WARNING - Target path NOT exists!")
                all_feather_year = []
            else:
                all_feather_list = os.listdir(path)
                all_feather_year = [int(i.split(".", maxsplit=1)[0].split("_")[-1]) for i in all_feather_list]
                
            return all_feather_year
        except Exception as e:
            raise e

    def __create_directory(self, target_directory: str):
        self.auth_check()
        
        if not os.path.exists(target_directory):
            os.makedirs(target_directory)

    def _append_feather(
        self,
        upload_df: pd.DataFrame,
        feather_path: str,
        factor_code: str,
        data_columns: List[str],
        sort_columns: List[str],
        all_feather_year: List[int],
    ):
        """
        添加数据到feather文件
    
        Parameters
        ----------
            upload_df : pd.DataFrame
                股票日线数据
            feather_path : str
                feather文件路径
            factor_code : str
                因子代码
            data_columns : List[str]
                列名
            sort_columns : List[str]
                用于排序的列组合
            all_feather_year : List[int]
                feather文件年份列表
        """
        self.auth_check()
        
        str_now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        
        if "TimeStamp" not in data_columns:
            data_columns.append("TimeStamp")
    
        if "TimeStamp" not in upload_df.columns:
            upload_df['TimeStamp'] = str_now
            
        feather_path = os.path.join(feather_path, "{factor_code}_{year}.feather".format(factor_code=factor_code,
                                                                                        year=upload_df.name))
        if upload_df.name in all_feather_year:
            df_feather: pd.DataFrame = feather.read_feather(feather_path)
            
            if "TimeStamp" not in df_feather.columns:
                df_feather['TimeStamp'] = str_now
                df_feather = df_feather[data_columns]
                
            # df_feather = df_feather.append(upload_df[data_columns], sort=False)
            # df_feather.drop_duplicates(subset=sort_columns, keep="last", inplace=True)
            df_feather = pd.concat([df_feather, upload_df[data_columns]], sort=False, ignore_index=True)
            df_feather.drop_duplicates(subset=sort_columns, keep="first", inplace=True)
            df_feather.sort_values(by=sort_columns, inplace=True, ignore_index=True)
            feather.write_feather(df_feather[data_columns], feather_path)
        else:
            feather.write_feather(upload_df[data_columns], feather_path)
    
    def __delete_factor_value(
        self,
        factor_code: str,
        security_code: str,
        security_type: Literal["Stock", "Index", "Future", "Option", "Bond", "Fund"],
        start_date: Union[str, int],
        end_date: Union[str, int],
    ):
        self.auth_check()
        
        int_start_date = int(start_date)
        int_end_date = int(end_date)
        start_year = int(int_start_date) // 10000
        end_year = int(int_end_date) // 10000
        
        if start_year == end_year:
            year_list: List[Union[str, int]] = [start_year]
        elif start_year < end_year:
            year_list: List[Union[str, int]] = range(start_year, end_year + 1, 1)
            
        security_code_col = "{security_type}Code".format(security_type=security_type)
    
        feather_file_directory = os.path.join(self.FEATHER_ROOT_PATH, "Data", security_type, "Factor", factor_code)
        all_feather_year = self.__get_factor_years(feather_file_directory)
        
        for iyear in year_list:
            if iyear not in all_feather_year:
                continue
                
            feather_path = os.path.join(feather_file_directory, "{factor_name}_{year}.feather".format(
                factor_name=factor_code, year=iyear))
            df_factor: pd.DataFrame = feather.read_feather(feather_path)
            
            if len(security_code) > 0:
                df_subset = df_factor[
                    (df_factor[security_code_col] == security_code)
                    & ((df_factor["TradingDay"] >= int_start_date) & (df_factor["TradingDay"] <= int_end_date))]
                df_factor = df_factor.loc[df_factor.index.difference(df_subset.index)]
            else:
                df_factor = df_factor[(df_factor["TradingDay"] < int_start_date) | (df_factor["TradingDay"] > int_end_date)]
                
            df_factor.reset_index(drop=True, inplace=True)
            
            if len(df_factor) == 0:
                os.remove(feather_path)
            else:
                feather.write_feather(df_factor, feather_path)

    def __delete_factor_value_table(
        self,
        factor_code: str,
        security_type: Literal["Stock", "Index", "Future", "Option", "Bond", "Fund"],
        security_code: str = "",
        remove_meta: bool = False
    ):
        self.auth_check()
        
        feather_file_directory = os.path.join(self.FEATHER_ROOT_PATH, "Data", security_type, "Factor", factor_code)
        all_feather_year = self.__get_factor_years(feather_file_directory)
        security_code_col = "{security_type}Code".format(security_type=security_type)
        if not security_code:
            for year in all_feather_year:
                feather_path = os.path.join(feather_file_directory, "{factor_name}_{year}.feather".format(
                        factor_name=factor_code, year=year))
                if os.path.exists(feather_path):
                    os.remove(feather_path)
            
            if os.path.exists(feather_file_directory):
                os.rmdir(feather_file_directory)
            
            if security_type in ["Stock", "Index"]:
                asset_type = 'Equity'
            else:
                asset_type = security_type
            
            if remove_meta:
                sql_query = ("call etiger_core.pSDKRemoveETFactor('{asset_type}', '{factor_code}', '{owner}');".format(
                    asset_type=asset_type, factor_code=factor_code, owner=self.user))
                da.executeScalarQueryInMYDB(sql_query)
        else:
            for year in all_feather_year:
                feather_path = os.path.join(feather_file_directory, "{factor_name}_{year}.feather".format(
                    factor_name=factor_code, year=year
                ))
                df_factor: pd.DataFrame = feather.read_feather(feather_path)
                df_factor = df_factor[df_factor[security_code_col] != security_code]
                df_factor.reset_index(drop=True, inplace=True)
                feather.write_feather(df_factor, feather_path)
    
    def __get_factor_value_daily(
        self,
        factor_code: str,
        security_type: Literal["Stock", "Index", "Future", "Option", "Bond", "Fund"],
        security_list: List[str] = list(),
        start_date: Optional[Union[int, str]] = None,
        end_date: Optional[Union[int, str]] = None,
        need_timestamp: Optional[bool] = False
    ) -> pd.DataFrame:
        self.auth_check()
        
        feather_file_directory = os.path.join(self.FEATHER_ROOT_PATH, "Data", security_type, "Factor", factor_code)
        all_feather_year = self.__get_factor_years(feather_file_directory)
    
        if start_date:
            start_year_int = int(start_date) // 10000
            all_feather_year = [i for i in all_feather_year if i >= start_year_int]
        if end_date:
            end_year_int = int(end_date) // 10000
            all_feather_year = [i for i in all_feather_year if i <= end_year_int]
            
        all_data: List[pd.DataFrame] = [
            feather.read_feather(os.path.join(feather_file_directory, "{factor_name}_{year}.feather".format(
                factor_name=factor_code, year=i)))
            for i in all_feather_year
        ]
    
        if factor_code.startswith('hf_') or factor_code.startswith('test_hf_'):
            time_column = 'TradingTime'
            security_code_col = "SecurityCode"
        else:
            time_column = 'TradingDay'
            security_code_col = "{security_type}Code".format(security_type=security_type)
            
        if need_timestamp:
            selected_cols = [time_column, security_code_col, "FactorValue", "TimeStamp"]
        else:
            selected_cols = [time_column, security_code_col, "FactorValue"]
        
        if all_data:
            df_factor_value: pd.DataFrame = pd.concat(all_data, axis=0, sort=False)
        else:
            df_factor_value = pd.DataFrame(columns=selected_cols)
            return df_factor_value
    
        if security_list:
            df_factor_value = df_factor_value[
                df_factor_value[security_code_col].isin(security_list)
            ]
            
        if not df_factor_value.columns.__contains__('TradingDay'):
            df_factor_value["TradingDay"] = df_factor_value["TradingTime"].map(
                lambda x: int(datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d")))
            
        if start_date:
            df_factor_value = df_factor_value[
                df_factor_value["TradingDay"] >= int(start_date)
            ]
        if end_date:
            df_factor_value = df_factor_value[
                df_factor_value["TradingDay"] <= int(end_date)
            ]
    
        df_factor_value = df_factor_value[selected_cols]
        
        df_factor_value.sort_values(
            by=[time_column, security_code_col], inplace=True, ignore_index=True
        )
        return df_factor_value
    
    def __save_factor_value(
        self,
        security_code: str,
        security_type: Literal["Stock", "Index", "Future", "Option", "Bond", "Fund"],
        factor_code: str,
        trading_day: Union[str, int],
        factor_value: float,
    ):
        self.auth_check()
        
        trading_day_int = int(trading_day)
        factor_year = int(trading_day_int) // 10000
        security_code_col = "{security_type}Code".format(security_type=security_type)
    
        feather_file_directory = os.path.join(self.FEATHER_ROOT_PATH, "Data", security_type, "Factor", factor_code)
        
        self.__create_directory(feather_file_directory)
        
        all_feather_year = self.__get_factor_years(feather_file_directory)
        if factor_year not in all_feather_year:
            df_new = pd.DataFrame(
                pd.DataFrame(
                    {
                        "TradingDay": [trading_day_int],
                        security_code_col: [security_code],
                        "FactorValue": [factor_value],
                    }
                )
            )
        else:
            feather_path = os.path.join(feather_file_directory, "{factor_name}_{year}.feather".format(
                factor_name=factor_code, year=factor_year))
            df_old: pd.DataFrame = feather.read_feather(feather_path)
            df_new = df_old.append(
                {
                    "TradingDay": trading_day_int,
                    security_code_col: security_code,
                    "FactorValue": factor_value,
                },
                ignore_index=True,
            )
            df_new.drop_duplicates(subset=["TradingDay", security_code_col], inplace=True)
        
        df_new.sort_values(
            by=["TradingDay", security_code_col], inplace=True, ignore_index=True
        )
        
        feather.write_feather(
            df_new,
            os.path.join(feather_file_directory, "{factor_name}_{year}.feather".format(
                factor_name=factor_code, year=factor_year))
        )
        return 1
    
    def __save_factor_value_DataFrame(
            self,
            factor_code: str,
            security_type: Literal["Stock", "Index", "Future", "Option", "Bond", "Fund"],
            var_dataframe: pd.DataFrame,
            data_columns: List[str],
            sort_columns: List[str],
    ):
        self.auth_check()
        
        feather_file_directory = os.path.join(self.FEATHER_ROOT_PATH, "Data", security_type, "Factor", factor_code)
        self.__create_directory(feather_file_directory)
        all_feather_year = self.__get_factor_years(feather_file_directory)
        
        if var_dataframe.empty:
            return
        var_dataframe["Year"] = var_dataframe["TradingDay"] // 10000
        for y in var_dataframe.Year.unique():
            df = var_dataframe[var_dataframe.Year == y]
            df.name = y
            self._append_feather(df, data_columns=data_columns,
                                 all_feather_year=all_feather_year,
                                 feather_path=feather_file_directory,
                                 factor_code=factor_code,
                                 sort_columns=sort_columns)

    def __save_hf_factor_value_DataFrame(
            self,
            factor_code: str,
            security_type: Literal["Stock", "Index", "Future", "Option", "Bond", "Fund"],
            var_dataframe: pd.DataFrame,
            data_columns: List[str],
            sort_columns: List[str],
    ):
        self.auth_check()
        
        feather_file_directory = os.path.join(self.FEATHER_ROOT_PATH, "Data", security_type, "Factor", factor_code)
        self.__create_directory(feather_file_directory)
        all_feather_year = self.__get_factor_years(feather_file_directory)
        
        if var_dataframe.empty:
            return
        var_dataframe["Year"] = var_dataframe["TradingTime"].map(lambda x: datetime.datetime
                                                                 .strptime(x, "%Y-%m-%d %H:%M:%S").year)
        var_dataframe.groupby("Year").apply(
            self._append_feather,
            data_columns=data_columns,
            all_feather_year=all_feather_year,
            feather_path=feather_file_directory,
            factor_code=factor_code,
            sort_columns=sort_columns,
        )
    
    def __record_factor_operation(self, caller: str, command: str, parameter: str):
        self.auth_check()
        
        sql_query = "call etiger_core.pSDKRecordFactorOperation('{0}', '{1}', '{2}');".format(caller, command, parameter)
        
        return da.executeScalarQueryInMYDB(sql_query)
    
    def __check_factor_owner(self, asset_type: str, factor_code: str):
        self.auth_check()
        
        sql_query = "call etiger_core.pSDKFactorOwnerCheck('{0}', '{1}', '{2}');".format(self.user, asset_type, factor_code)
        result = da.executeSqlQueryInMYDB(sql_query)
        
        return result['Result'][0]
    
    def save_hf_factor_value_DataFrame(
        self,
        factor_code: str,
        security_type: str,
        factor_dataframe: pd.DataFrame,
    ):
        """保存单个高频因子的数据，可以是多股票、多期货合约，多日，只需要将交易时间、因子值封装到DataFrame中
    
        DataFrame格式:
          | TradingTime   | SecurityCode     | FactorValue | \\
          | ------------ | ------------- | ----------- | \\
          | str YYYY-mm-dd HH:MM:SS| str dddddd.ss | float     | \\
          | 2022-06-30 09:45:00  | 600000.SH     | 1.2345      | \\
          | 2022-06-30 09:45:00  | 000001.SZ     | 2.1234      | \\
          | 2022-06-30 09:50:00  | 600000.SH     | 1.2347      | \\
          | 2022-06-30 09:50:00  | 000001.SZ     | 2.1235      |
    
        Parameters
        ----------
            factor_code : str, 因子代码
            security_type: str, 金融产品类型，比如"Stock", "Index", "Future", "Option", "Bond", "Fund"
            factor_dataframe : pd.DataFrame, 因子数据
    
        Examples
        --------
            >>> ETSDK.save_hf_factor_value_DataFrame(
            ...     factor_code = "Test_Factor",
            ...     security_type = "Stock",
            ...     factor_dataframe = pd.DataFrame(
            ...         {  "TradingTime": ['2022-06-30 09:45:00', '2022-06-30 09:45:00', '2022-06-30 09:50:00'],
            ...            "SecurityCode": ["600000.SH", "000001.SZ", "600000.SH"],
            ...            "FactorValue": [1.2345, 2.1234, 1.2347],
            ...         }
            ...     ),
            ...  )
        """
        self.auth_check()
        
        self.__save_hf_factor_value_DataFrame(
            factor_code=factor_code,
            security_type=security_type,
            var_dataframe=factor_dataframe,
            data_columns=["TradingTime", "SecurityCode", "FactorValue"],
            sort_columns=["TradingTime", "SecurityCode"],
        )
    
    # ========= 02 股票因子 =======================================================================
    # 创建股票因子分类的大类
    def create_stock_factor_parent_category(self, parent_category: str, parent_category_en: str):
        """创建股票因子分类的大类
    
        Parameters
        ----------
            parent_category : str
                大类名称
            parent_category_en : str
                大类英文名称
        """
        self.auth_check()
        
        sql_query = (
            "call etiger_core.pSDKCreateETFactorParentCategory('Equity', '{0}', '{1}');".format(
                parent_category, parent_category_en
            )
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 创建股票因子分类
    def create_stock_factor_category(self, parent_category: str, category: str, category_en: str):
        """创建股票因子分类
    
        Parameters
        ----------
            parent_category : str
                大类名称
            category : str
                分类名称
            category_en : str
                分类英文名称
        """
        self.auth_check()
        
        sql_query = (
            "call etiger_core.pSDKCreateETFactorCategory('Equity', '{0}', '{1}', '{2}');".format(
                parent_category, category, category_en
            )
        )
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 创建股票因子
    def create_stock_factor(
        self,
        parent_category: str,
        category: str,
        factor_code: str,
        description: str,
        level: str,
        suggested_update_time: str,
        remark: str = "",
        factor_type: str = "",
    ):
        self.auth_check()
        
        self.create_et_factor(
            asset_type='Equity',
            parent_category=parent_category,
            category=category,
            factor_code=factor_code,
            description=description,
            level=level,
            suggested_update_time=suggested_update_time,
            remark=remark,
            factor_type=factor_type)
        return
    
    # 获取股票因子大类
    def get_stock_factor_parent_category(self) -> pd.DataFrame:
        """获取股票因子大类
        获取弈泰数据平台上所有来源的因子大类"""
        self.auth_check()
        
        conn = da.getFIDMySQLConnection()
        sql_query = "call etiger_core.pSDKGetETFactorParentCategory('Equity');"
    
        df = pd.read_sql(sql=sql_query, con=conn)
        conn.close()
        return df
    
    # 获取股票因子类
    def get_stock_factor_category(self) -> pd.DataFrame:
        """获取股票因子类
        获取弈泰数据平台上所有来源的因子分类及大类"""
        self.auth_check()
        
        conn = da.getFIDMySQLConnection()
        sql_query = "call etiger_core.pSDKGetETFactorCategory('Equity');"
    
        df = pd.read_sql(sql=sql_query, con=conn)
        conn.close()
        return df
    
    # 获取股票因子分类信息
    def get_stock_factor(self) -> pd.DataFrame:
        """获取股票因子分类信息
        获取弈泰数据平台上所有Equity的因子及其分类数据"""
        self.auth_check()
        
        conn = da.getFIDMySQLConnection()
        sql_query = "call etiger_core.pSDKGetETFactor('Equity');"
    
        df = pd.read_sql(sql=sql_query, con=conn)
        conn.close()
        return df
    
    # 获取因子Id或FactorId
    def get_stock_factor_id(self, factor_code: str) -> Optional[int]:
        """获取因子Id或FactorId
    
        Parameters
        ----------
            factor_code : str
                因子代码
        """
        self.auth_check()
        
        sql_query = "call etiger_core.pSDKGetETFactorId('Equity', '{0}');".format(factor_code)
        df = da.executeSqlQueryInMYDB(sql_query)
    
        if not df.empty:
            return df.iloc[0, 0]
    
    # 停用股票因子
    def disable_stock_factor(self, factor_code: str):
        """停用股票因子
    
        Parameters
        ----------
            factor_code : str, 因子代码
        """
        self.auth_check()
        
        sql_query = "call etiger_core.pSDKDisableETFactor('Equity', '{0}', '{1}');".format(factor_code, self.user)
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    # 更新股票因子
    def update_stock_factor(
        self,
        factor_id: Union[str, int],
        category_id: Union[str, int] = None,
        factor_code: str = None,
        description: str = None,
        level: str = None,
        suggested_update_time: str = None,
        remark: str = None,
        factor_type: str = None,
    ):
        """更新股票因子，factor_id是主键，其它均可选
    
        Parameters
        ----------
            factor_id : str, 因子Id
            category_id : str, 分类Id
            factor_code : str, 因子代码
            description : str, 有关该因子的描述
            level : str, 因子与其它因子的关系，衍生因子的层次，比如：'2'
            suggested_update_time : str, 建议更新因子的时间，比如：'T0 22:00'
            remark : str, 备注或说明
            factor_type : str, 取值包括directional和relative_value，用于说明因子的取值属性，是定性的还是定量的
        """
        self.auth_check()
        
        df = self.get_et_factor()
        df = df[df['FactorId'] == factor_id]
        df.reset_index(inplace=True)
        
        category_id_db = df.loc[0, 'CategoryId']
        factor_code_db = df.loc[0, 'FactorCode']
        description_db = df.loc[0, 'Description']
        level_db = df.loc[0, 'Level']
        suggested_update_time_db = df.loc[0, 'SuggestedUpdateTime']
        remark_db = df.loc[0, 'Remark']
        factor_type_db = df.loc[0, 'FactorType']
        
        if not category_id:
            category_id = category_id_db
        if not factor_code:
            factor_code = factor_code_db
        if not description:
            description = description_db
        if not level:
            level = level_db
        if not suggested_update_time:
            suggested_update_time = suggested_update_time_db
        if not remark:
            remark = remark_db
        if not factor_type:
            factor_type = factor_type_db
        
        sql_query = "call etiger_core.pSDKUpdateETFactor({0}, {1}, '{2}', '{3}', '{4}', '{5}', '{6}', '{7}');"\
            .format(str(factor_id),
                    str(category_id),
                    factor_code,
                    description,
                    level,
                    suggested_update_time,
                    remark,
                    factor_type)
    
        return da.executeScalarQueryInMYDB(sql_query)
    
    def save_stock_factor_value(
        self, stock_code: str, factor_code: str, trading_day: Union[str, int], factor_value: float
    ):
        """保存股票因子值
    
        Parameters
        --------
            stock_code: str, 股票代码
            factor_code: str, 因子代码
            trading_day: Union[str, int], 交易日
            factor_value: float, 因子值
    
        Examples
        --------
            >>> ETSDK.save_stock_factor_value('600000.SH', 'BollDown', 20211125, 1.2345)
        """
        self.auth_check()
        
        self.__save_factor_value(stock_code, "Stock", factor_code, trading_day, factor_value)
    
    def save_stock_factor_value_DataFrame(
        self,
        factor_code: str,
        factor_dataframe: pd.DataFrame,
    ):
        """保存单个股票因子的数据，可以是多股票，多日，只需要将股票、日期、因子值封装到DataFrame中
    
        DataFrame格式:
          | TradingDay   | StockCode     | FactorValue | \\
          | ------------ | ------------- | ----------- | \\
          | int yyyymmdd | str dddddd.ss | float       | \\
          | 20211008     | 600000.SH     | 1.2345      | \\
          | 20211125     | 000001.SZ     | 2.1234      |
    
        Parameters
        ----------
            factor_code : str, 因子代码
            factor_dataframe : pd.DataFrame, 因子数据
    
        Examples
        --------
            >>> ETSDK.save_stock_factor_value_DataFrame(
            ...     factor_code = "BollDown",
            ...     factor_dataframe = pd.DataFrame(
            ...         {  "TradingDay": [20211008, 20211125],
            ...            "StockCode": ["600000.SH", "000001.SZ"],
            ...            "FactorValue": [1.2345, 2.1234],
            ...         }
            ...     ),
            ...  )
        """
        self.auth_check()
        
        self.__save_factor_value_DataFrame(
            factor_code=factor_code,
            security_type="Stock",
            var_dataframe=factor_dataframe,
            data_columns=["TradingDay", "StockCode", "FactorValue"],
            sort_columns=["TradingDay", "StockCode"],
        )
    
    def get_stock_factor_value_daily(
        self,
        factor_code: str,
        stock_list: List[str] = list(),
        start_date: Optional[Union[int, str]] = None,
        end_date: Optional[Union[int, str]] = None,
        need_timestamp: Optional[bool] = False
    ) -> pd.DataFrame:
        """获取股票因子日行情
    
        parameters
        ----------
            factor_code: 因子代码
            stock_list: 股票列表,默认为空列表,即获取所有股票数据
            start_date: 开始日期,默认为None,即获取存储的最早日期
            end_date: 结束日期,默认为None,即获取存储的最新日期
            need_timestamp: 是否需要输出Timestamp列，正常情况下是不需要的；Timestamp列仅用于审计目的，确保数据按照预期的时间进行了更新
    
        Examples
        --------
            # 保持后向兼容，避免对现有数据使用造成影响
            >>> ETSDK.get_stock_factor_value_daily(
            ...     factor_code = "BollDown",   #因子代码
            ...     stock_list = ["000001.SZ","000002.SZ"], #股票列表
            ...     start_date = "20200101", #开始日期
            ...     end_date = "20200130", #结束日期
            ... )
            # 获取TimeStamp列的示例代码
            >>> ETSDK.get_stock_factor_value_daily(
            ...     factor_code = "BollDown",   #因子代码
            ...     stock_list = ["000001.SZ","000002.SZ"], #股票列表
            ...     start_date = "20200101", #开始日期
            ...     end_date = "20200130", #结束日期
            ...     need_timestamp = True, #需要输出TimeStamp列以对数据进行审计
            ... )
        """
        self.auth_check()
        
        command = "get_stock_factor_value_daily"
        parameter = "({0}，Stock, stock_list, {1}, {2}, {3})".format(factor_code, start_date, end_date, need_timestamp)
        self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
        
        return self.__get_factor_value_daily(
            factor_code=factor_code,
            security_type="Stock",
            security_list=stock_list,
            start_date=start_date,
            end_date=end_date,
            need_timestamp=need_timestamp,
        )
    
    def delete_stock_factor_value(
            self,
            factor_code: str,
            stock_code: str,
            start_date: Union[str, int],
            end_date: Union[str, int]
    ):
        """删除指定 factor_code 中指定 stock_code, start_date, end_date 的因子数据
    
        Examples
        --------
            # 删除特定因子，特定股票，在特定时间段内的数据
            >>> ETSDK.delete_stock_factor_value(
            ...     factor_code='hf_volume1', stock_code='600000.SH', start_date=20200101, end_date=20200103
            ... )
            # 删除特定因子，在特定时间段内的全部数据
            >>> ETSDK.delete_stock_factor_value(
            ...     factor_code='hf_volume1', stock_code='', start_date=20200101, end_date=20200103
            ... )
        """
        self.auth_check()
        
        is_factor_owner = self.__check_factor_owner('Equity', factor_code)
        if is_factor_owner:
            command = "delete_stock_factor_value"
            parameter = "({0}，Stock, stock_code, {1}, {2})".format(factor_code, start_date, end_date)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__delete_factor_value(factor_code, stock_code, "Stock", start_date, end_date)
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
    
    def delete_stock_factor_value_table(
            self,
            factor_code: str,
            stock_code: str = "",
            remove_meta: bool = False):
        """删除指定 factor_code 中指定 stock_code 的因子数据
        如果 stock_code为空,则删除 factor_code 的所有因子值数据,保留数据库中的因子元数据；
        如果要删除因子的全部数据及数据库中的因子元数据，需要将参数remove_meta设置为True；
    
        Examples
        --------
            >>> ETSDK.delete_stock_factor_value_table(factor_code='hf_volume1', stock_code='600000.SH')
            >>> ETSDK.delete_stock_factor_value_table(factor_code='hf_volume1')
            >>> ETSDK.delete_stock_factor_value_table(factor_code='hf_volume1', remove_meta=True)
        """
        self.auth_check()
        
        is_factor_owner = self.__check_factor_owner('Equity', factor_code)
        if is_factor_owner:
            command = "delete_stock_factor_value_table"
            parameter = "({0}，Stock, stock_code, {1})".format(factor_code, remove_meta)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__delete_factor_value_table(factor_code=factor_code,
                                             security_type="Stock",
                                             security_code=stock_code,
                                             remove_meta=remove_meta)
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
    
    def save_index_factor_value(
            self,
            index_code: str,
            factor_code: str,
            trading_day: Union[str, int],
            factor_value: float
    ):
        """保存指数因子值
    
        Parameters
        --------
            index_code: str, 指数代码
            factor_code: str, 因子代码
            trading_day: Union[str, int], 交易日
            factor_value: float, 因子值
    
        Examples
        --------
            >>> ETSDK.save_index_factor_value('801010.SW', 'BollDown', 20211125, 1.2345)
        """
        self.__save_factor_value(index_code, "Index", factor_code, trading_day, factor_value)
    
    def save_index_factor_value_DataFrame(
            self,
            factor_code: str,
            factor_dataframe: pd.DataFrame,
    ):
        """保存单个指数因子的数据，可以是多日，多股票，只需要将日期、指数、因子值封装到DataFrame中
    
        DataFrame格式:
          | TradingDay   | IndexCode     | FactorValue | \\
          | ------------ | ------------- | ----------- | \\
          | int yyyymmdd | str dddddd.ss | float       | \\
          | 20211008     | 801010.SW     | 1.2345      | \\
          | 20211125     | 801030.SW     | 2.1234      |
    
        Parameters
        ----------
            factor_code : str, 因子代码
            factor_dataframe : pd.DataFrame, 因子数据
    
        Examples
        --------
            >>> ETSDK.save_index_factor_value_DataFrame(
            ...     factor_code = "BollDown",
            ...     factor_dataframe = pd.DataFrame(
            ...         {  "TradingDay": [20211008, 20211125],
            ...            "IndexCode": ["801010.SW", "801030.SW"],
            ...            "FactorValue": [1.2345, 2.1234],
            ...         }
            ...     ),
            ...  )
        """
        self.__save_factor_value_DataFrame(
            factor_code=factor_code,
            security_type="Index",
            var_dataframe=factor_dataframe,
            data_columns=["TradingDay", "IndexCode", "FactorValue"],
            sort_columns=["TradingDay", "IndexCode"],
        )
    
    def get_index_factor_value_daily(
            self,
            factor_code: str,
            index_list: List[str] = list(),
            start_date: Optional[Union[int, str]] = None,
            end_date: Optional[Union[int, str]] = None,
            need_timestamp: Optional[bool] = False
    ) -> pd.DataFrame:
        """获取指数因子日行情
    
        parameters
        ----------
            factor_code: 因子代码
            index_list: 指数列表,默认为空列表,即获取所有指数数据
            start_date: 开始日期,默认为None,即获取存储的最早日期
            end_date: 结束日期,默认为None,即获取存储的最新日期
            need_timestamp: 是否需要输出Timestamp列，正常情况下是不需要的；Timestamp列仅用于审计目的，确保数据按照预期的时间进行了更新
    
        Examples
        --------
            # 保持后向兼容，避免对现有数据使用造成影响
            >>> ETSDK.get_index_factor_value_daily(
            ...     factor_code = "BollDown",   #因子代码
            ...     index_list = ["801010.SW", "801030.SW"], #指数列表
            ...     start_date = 20200101, #开始日期
            ...     end_date = 20200130, #结束日期
            ... )
            # 获取TimeStamp列的示例代码
            >>> ETSDK.get_index_factor_value_daily(
            ...     factor_code = "BollDown",   #因子代码
            ...     index_list = ["801010.SW", "801030.SW"], #指数列表
            ...     start_date = 20200101, #开始日期
            ...     end_date = 20200130, #结束日期
            ...     need_timestamp = True, #需要输出TimeStamp列以对数据进行审计
            ... )
        """
        self.auth_check()
        
        command = "get_index_factor_value_daily"
        parameter = "({0}，index_list, {1}, {2}, {3})".format(factor_code, start_date, end_date, need_timestamp)
        self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
        
        return self.__get_factor_value_daily(
            factor_code=factor_code,
            security_type="Index",
            security_list=index_list,
            start_date=start_date,
            end_date=end_date,
            need_timestamp=need_timestamp,
        )
    
    def delete_index_factor_value(
            self,
            factor_code: str,
            index_code: str,
            start_date: Union[str, int],
            end_date: Union[str, int]
    ):
        """删除指定 factor_code 中指定 index_code, start_date, end_date 的因子数据
    
        Examples
        --------
            # 删除特定因子，特定指数，在特定时间段内的数据
            >>> ETSDK.delete_index_factor_value(
            ...     factor_code="hf_volume1", index_code="801010.SW", start_date=20200101, end_date=20200103
            ... )
            # 删除特定因子，在特定时间段内的全部数据
            >>> ETSDK.delete_index_factor_value(
            ...     factor_code="hf_volume1", index_code="", start_date=20200101, end_date=20200103
            ... )
        """
        self.auth_check()

        is_factor_owner = self.__check_factor_owner('Equity', factor_code)
        if is_factor_owner:
            command = "delete_index_factor_value"
            parameter = "({0}，index_code, {1}, {2})".format(factor_code, start_date, end_date)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__delete_factor_value(factor_code, index_code, "Stock", start_date, end_date)
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
            
    def delete_index_factor_value_table(
            self,
            factor_code: str,
            index_code: str = "",
            remove_meta: bool = False):
        """删除指定 factor_code 中指定 index_code 的因子数据
        如果 index_code为空,则删除 factor_code 的所有因子值数据,保留数据库中的因子元数据；
        如果要删除因子的全部数据及数据库中的因子元数据，需要将参数remove_meta设置为True；
    
        Examples
        --------
            >>> ETSDK.delete_index_factor_value_table(factor_code='hf_volume1', index_code='801010.SW')
            >>> ETSDK.delete_index_factor_value_table(factor_code='hf_volume1')
            >>> ETSDK.delete_index_factor_value_table(factor_code='hf_volume1', remove_meta=True)
        """
        self.auth_check()

        is_factor_owner = self.__check_factor_owner('Equity', factor_code)
        if is_factor_owner:
            command = "delete_index_factor_value_table"
            parameter = "({0}，index_code, {1})".format(factor_code, remove_meta)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__delete_factor_value_table(factor_code=factor_code,
                                             security_type="Index",
                                             security_code=index_code,
                                             remove_meta=remove_meta)
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
    
    # ========= 03 期货因子 =======================================================================
    # 获取期货因子类
    def get_future_factor_category(self) -> pd.DataFrame:
        """获取期货因子类
        获取弈泰数据平台上所有来源的因子分类及大类"""
        self.auth_check()
        
        conn = da.getFIDMySQLConnection()
        sql_query = "call etiger_core.pSDKGetETFactorCategory('Future');"
    
        df = pd.read_sql(sql=sql_query, con=conn)
        conn.close()
        return df
    
    # 获取期货因子分类信息
    def get_future_factor(self) -> pd.DataFrame:
        """获取期货因子分类信息
        获取弈泰数据平台上所有Future的因子及其分类数据"""
        self.auth_check()
        
        conn = da.getFIDMySQLConnection()
        sql_query = "call etiger_core.pSDKGetETFactor('Future');"
    
        df = pd.read_sql(sql=sql_query, con=conn)
        conn.close()
        return df
    
    def save_future_factor_value_DataFrame(
            self,
            factor_code: str,
            factor_dataframe: pd.DataFrame,
    ):
        """保存单个期货因子的数据，可以是多日，多合约，只需要将日期、合约、因子值封装到DataFrame中
    
        DataFrame格式:
          | TradingDay   | FutureCode     | FactorValue | \\
          | ------------ | ------------- | ----------- | \\
          | int yyyymmdd | str dddddd | float       | \\
          | 20211008     | IF2206     | 1.2345      | \\
          | 20211125     | IF2207    | 2.1234      |
    
        Parameters
        ----------
            factor_code : str, 因子代码
            factor_dataframe : pd.DataFrame, 因子数据
    
        Examples
        --------
            >>> ETSDK.save_future_factor_value_DataFrame(
            ...     factor_code = "BollDown",
            ...     factor_dataframe = pd.DataFrame(
            ...         {  "TradingDay": [20211008, 20211125],
            ...            "FutureCode": ["IF2206", "IF2207"],
            ...            "FactorValue": [1.2345, 2.1234],
            ...         }
            ...     ),
            ...  )
        """
        self.auth_check()

        is_factor_owner = self.__check_factor_owner('Future', factor_code)
        if is_factor_owner:
            command = "save_future_factor_value_DataFrame"
            parameter = "({0}，factor_dataframe)".format(factor_code)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__save_factor_value_DataFrame(
                factor_code=factor_code,
                security_type="Future",
                var_dataframe=factor_dataframe,
                data_columns=["TradingDay", "FutureCode", "FactorValue"],
                sort_columns=["TradingDay", "FutureCode"])
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
    
    def get_future_factor_value_daily(
            self,
            factor_code: str,
            future_list: List[str] = list(),
            start_date: Optional[Union[int, str]] = None,
            end_date: Optional[Union[int, str]] = None,
            need_timestamp: Optional[bool] = False
    ) -> pd.DataFrame:
        """获取期货因子日行情
    
        parameters
        ----------
            factor_code: 因子代码
            future_list: 期货列表,默认为空列表,即获取所有指数数据
            start_date: 开始日期,默认为None,即获取存储的最早日期
            end_date: 结束日期,默认为None,即获取存储的最新日期
            need_timestamp: 是否需要输出Timestamp列，正常情况下是不需要的；Timestamp列仅用于审计目的，确保数据按照预期的时间进行了更新
    
        Examples
        --------
            # 保持后向兼容，避免对现有数据使用造成影响
            >>> ETSDK.get_future_factor_value_daily(
            ...     factor_code = "BollDown",   #因子代码
            ...     future_list = ["IF2206", "IC2207"], #期货列表
            ...     start_date = 20200101, #开始日期
            ...     end_date = 20200130, #结束日期
            ... )
            # 获取TimeStamp列的示例代码
            >>> ETSDK.get_future_factor_value_daily(
            ...     factor_code = "BollDown",   #因子代码
            ...     future_list = ["IF2206", "IC2207"], #期货列表
            ...     start_date = 20200101, #开始日期
            ...     end_date = 20200130, #结束日期
            ...     need_timestamp = True, #需要输出TimeStamp列以对数据进行审计
            ... )
        """
        self.auth_check()
        
        command = "get_future_factor_value_daily"
        parameter = "({0}，future_contract_list, {1}, {2}, {3})".format(factor_code, start_date, end_date, need_timestamp)
        self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
        
        return self.__get_factor_value_daily(
            factor_code=factor_code,
            security_type="Future",
            security_list=future_list,
            start_date=start_date,
            end_date=end_date,
            need_timestamp=need_timestamp,
        )
    
    def delete_future_factor_value(
            self,
            factor_code: str,
            future_code: str,
            start_date: Union[str, int],
            end_date: Union[str, int]
    ):
        """删除指定 factor_code 中指定 future_code, start_date, end_date 的因子数据
    
        Examples
        --------
            # 删除特定因子，特定合约，在特定时间段内的数据
            >>> ETSDK.delete_future_factor_value(
            ...     factor_code="hf_volume1", future_code="IF2206", start_date=20200101, end_date=20200103
            ... )
            # 删除特定因子，在特定时间段内的全部数据
            >>> ETSDK.delete_future_factor_value(
            ...     factor_code="hf_volume1", future_code="", start_date=20200101, end_date=20200103
            ... )
        """
        self.auth_check()

        is_factor_owner = self.__check_factor_owner('Future', factor_code)
        if is_factor_owner:
            command = "delete_future_factor_value"
            parameter = "({0}，future_code, {1}, {2})".format(factor_code, start_date, end_date)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__delete_factor_value(factor_code, future_code, "Future", start_date, end_date)
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
        
    def delete_future_factor_value_table(
            self,
            factor_code: str,
            future_code: str = "",
            remove_meta: bool = False):
        """删除指定 factor_code 中指定 future_code 的因子数据
        如果 future_code,则删除 factor_code 的所有因子值数据,保留数据库中的因子元数据；
        如果要删除因子的全部数据及数据库中的因子元数据，需要将参数remove_meta设置为True；
    
        Examples
        --------
            >>> ETSDK.delete_future_factor_value_table(factor_code='hf_volume1', future_code='IF2206')
            >>> ETSDK.delete_future_factor_value_table(factor_code='hf_volume1')
            >>> ETSDK.delete_future_factor_value_table(factor_code='hf_volume1', remove_meta=True)
        """
        self.auth_check()

        is_factor_owner = self.__check_factor_owner('Future', factor_code)
        if is_factor_owner:
            command = "delete_future_factor_value_table"
            parameter = "({0}，future_code, {1})".format(factor_code, remove_meta)
            self.__record_factor_operation(caller=self.user, command=command, parameter=parameter)
            
            self.__delete_factor_value_table(factor_code=factor_code,
                                             security_type="Future",
                                             security_code=future_code,
                                             remove_meta=remove_meta)
        else:
            print("Action ignored because you are NOT the owner of the factor ---> {0}".format(factor_code))
        
    # ========= 04 因子评估 =======================================================================
    def get_factor_assessment(
            self,
            asset_type: Optional[Literal["Equity", "Future", "Option", "Bond", "Fund"]] = None,
            factor_code: Optional[str] = None
    ) -> pd.DataFrame:
        """获取因子评估信息
        Examples
        --------
            >>> ETSDK.get_factor_assessment()
            >>> ETSDK.get_factor_assessment(asset_type='Future')
            >>> ETSDK.get_factor_assessment(factor_code='ACCA')
            >>> ETSDK.get_factor_assessment(asset_type='Equity', factor_code='ACCA')
        """
        self.auth_check()
        
        if asset_type and factor_code:
            sql_query = "call etiger_core.pSDKGetFactorAssessment('{0}', '{1}');".format(asset_type, factor_code)
        elif (not asset_type) and factor_code:
            sql_query = "call etiger_core.pSDKGetFactorAssessment(NULL, '{0}');".format(factor_code)
        elif asset_type and (not factor_code):
            sql_query = "call etiger_core.pSDKGetFactorAssessment('{0}', NULL);".format(asset_type)
        else:
            sql_query = "call etiger_core.pSDKGetFactorAssessment(NULL, NULL);"
            
        df = da.executeSqlQueryInMYDB(sql_query)
    
        return df
    
    # 保存/更新因子评估数据
    def save_factor_assessment(
            self,
            asset_type: Literal["Equity", "Future", "Option", "Bond", "Fund"],
            factor_code: str,
            in_sample_start: Optional[str] = None,
            in_sample_end: Optional[str] = None,
            back_test_days: Optional[Union[str, int]] = None,
            back_test_avg_turnover: Optional[Union[str, float]] = None,
            back_test_sharp: Optional[Union[str, float]] = None,
            out_sample_end: Optional[str] = None,
            out_sample_days: Optional[Union[str, int]] = None,
            out_sample_sharp: Optional[Union[str, float]] = None,
            is_valid: Optional[int] = 0,
            valid_until: Optional[str] = None,
            factor_type: Optional[Literal["directional", "relative_value"]] = "relative_value"
    ):
        """
        Parameters
        ----------
            asset_type : str, 资产类型，必填，可取值包括Equity, Future, Option, Bond, Fund
            factor_code : str, 必填，因子代码
            in_sample_start : str, 回测起始日期
            in_sample_end : str, 回测结束日期
            back_test_days : int, 回测天数
            back_test_avg_turnover : float, 回测结果 - 平均换手率
            back_test_sharp : str, 建议更新因子的时间，比如：'T0 22:00'
            out_sample_end : str, 有关该因子的描述
            out_sample_days : str, 该因子的权利人
            out_sample_sharp : str, 建议更新因子的时间，比如：'T0 22:00'
            is_valid : int, 默认为0 - 开发评估阶段；1 - 经评估满足要求，应用与生产；-1 - 评估不合格，将从系统中被清除；
            valid_until : str, 有效期截止
            factor_type : str, 取值包括directional和relative_value，用于说明因子的取值属性，是定性的还是定量的
        """
        self.auth_check()
    
        if not asset_type:
            raise "asset_type是必须指定的参数，可取值：Equity, Future, Option, Bond, Fund"
        if not factor_code:
            raise "factor_code是必须指定的参数"
    
        df = self.get_factor_assessment()
        df = df[(df['AssetType'] == asset_type) & (df['FactorCode'] == factor_code)]
        
        if len(df) > 0:
            df.reset_index(inplace=True)
            
            in_sample_start_db = df.loc[0, 'InSampleStart']
            in_sample_end_db = df.loc[0, 'InSampleEnd']
            back_test_days_db = df.loc[0, 'BT_Days']
            back_test_avg_turnover_db = df.loc[0, 'BT_AvgTurnover']
            back_test_sharp_db = df.loc[0, 'BT_Sharp']
            out_sample_end_db = df.loc[0, 'OutSampleEnd']
            out_sample_days_db = df.loc[0, 'OS_Days']
            out_sample_sharp_db = df.loc[0, 'OS_Sharp']
            is_valid_db = df.loc[0, 'IsValid']
            valid_until_db = df.loc[0, 'ValidUntil']
            factor_type_db = df.loc[0, 'FactorType']
    
            if not in_sample_start:
                in_sample_start = in_sample_start_db
            if not in_sample_end:
                in_sample_end = in_sample_end_db
            if not back_test_days:
                back_test_days = back_test_days_db
            if not back_test_avg_turnover:
                back_test_avg_turnover = back_test_avg_turnover_db
            if not back_test_sharp:
                back_test_sharp = back_test_sharp_db
            if not out_sample_end:
                out_sample_end = out_sample_end_db
            if not out_sample_days:
                out_sample_days = out_sample_days_db
            if not out_sample_sharp:
                out_sample_sharp = out_sample_sharp_db
            if not is_valid:
                is_valid = is_valid_db
            if not valid_until:
                valid_until = valid_until_db
            if not factor_type:
                factor_type = factor_type_db
            remark = "by {0}".format(self.user)
    
        sql_query = "call etiger_core.pSDKSaveFactorAssessment('{0}', '{1}', '{2}', '{3}', {4}, {5}, {6}, '{7}', {8}" \
                    ", {9}, {10}, '{11}', '{12}', '{13}');" \
            .format(asset_type,
                    factor_code,
                    str(in_sample_start),
                    str(in_sample_end),
                    back_test_days,
                    back_test_avg_turnover,
                    back_test_sharp,
                    str(out_sample_end),
                    out_sample_days,
                    out_sample_sharp,
                    is_valid,
                    str(valid_until),
                    factor_type,
                    remark)
        sql_query = sql_query.replace("'None'", 'NULL').replace('nan', 'NULL')
    
        return da.executeScalarQueryInMYDB(sql_query)
